第5回 Spring環境におけるDBアクセス(2) 〜 Spring Data篇
よく訓練されたアップル信者、都元です。前回は散々説明しといて「こんなの使わないッスよね(クッチャクッチャ」っていう酷いオチでごめんなさいごめんなさい。えっと、今回が本命です。これが、私が日常的に使ってるデータアクセスの仕組みです。間違いねえっす。
ただし、前回の最後の注釈でも指摘した通り、便利で高水準なAPIは黒魔術的要素が強くなります。APIの水準と裏側の見通し *1はトレードオフですので、プロジェクトメンバーのスキル等も勘案しつつ、慎重な選択が必要なところかと思います。本連載の読者の皆様は基本的にSpringを学び始めて間もないと思っていますので、今まではSpringの中でもそこそこ見通しの良い(まだ魔術とは言えないような)機能を中心にご紹介してきました。一方、今回は比較的黒いです。なので今回はこのエントリを通じて、Spring Dataの高水準のデータアクセスAPIを学ぶだけではなく「黒魔術とはどういった状況のことを言っているのか」という所も、前回の低水準APIとの対比から、感じ取って頂ければと思います。
Spring Data
Spring Dataは、データストアに対するアクセスを抽象化するライブラリです。データストアとは、RDBのことだけではなく、MongoDB, Redis, Neo4j等のための実装も提供されています。
基本的な考え方は至って簡単。各データストアをRepositoryというインターフェイスに抽象化します。Repositoryとは、エンティティオブジェクトに対して、言わばコレクションのように振る舞うインターフェイスです。(実際にメソッド名は違いますが)オブジェクトを、addしたりremoveしたりselectしたりすることにより、データストアの操作を行う、という考え方です。詳しくは「エリック・エヴァンスのドメイン駆動設計」をご覧下さい。
リポジトリのインターフェイス
コードを見た方が分かりやすいですかね。まぁ、こんな感じのメソッドを取り揃えるのがリポジトリです。Tが保存対象のオブジェクトの型、IDが対象を特定するためのIDの型です。前回のusersテーブルについては、TがUser、主キーがusernameになっていますので、IDはStringに相当します。
public interface Repository<T, ID extends Serializable> { } public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { <S extends T> S save(S entity); T findOne(ID id); boolean exists(ID id); Iterable<T> findAll(); long count(); void delete(ID id); // 一部省略 } public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); }
spring-data-commonsとその実装たち
さて、このようなインターフェイス(これをspring-data-commonsというプロジェクトが提供します)に対して、RDBやMongo等に対応する各種実装(spring-data-jpaやspring-data-mongodb)が提供されています。今回はRDBへのアクセスを行いたいので、RDB用の実装を選ばなくてはならないのですが…。Spring Dataプロジェクト本家が提供するRDB用の実装は、JPAをラップする実装しか無いのですよ。
Spring Dataは好きだけどJPAは正直あまり… *2。よし、無いなら作るよ!
筆者が個人的に好きなデータアクセスフレームワークというのは、ご存知の通り(?)Mirage SQLですので、これをラップするSpring Data実装「spring-data-mirage」を作ってみました *3。Apache License, Version 2.0による許諾の下でご自由にご利用ください。
spring-data-mirage
さて、ここからが本番。以下の情報は他のどこにも書いてませんよ〜。だってここで初めて発表するんですからw というわけで、spring-data-mirageのプロダクト発表会の始まりです。
spring-data-mirageの顔となるサイトはひとまず当分はgithubです *4。で、自画自賛で恐縮なんですが、とっても使いやすいですw しかし、この使いやすさはMirage SQLとSpring Dataのお陰です。多分私の功績じゃありませんw
それはそうと、早速使ってみましょう。前提知識としてMirage SQL 〜 2WaySQLをつかうデータアクセスライブラリ for Javaをさらっと読んでおいてください。
依存ライブラリの定義
残念ながら、Mirage SQLもspring-data-mirageも、Mavenのcentralリポジトリには上がっていません *5。従って、repositoresにリポジトリURLの指定が必要です。
springDataMirageVersion = 0.3.3.RELEASE
repositories { // ... maven { url "http://maven.xet.jp/release" } // for spring-data-mirage } dependencies { // ... compile "org.springframework.data:spring-data-mirage:$springDataMirageVersion" }
Springの設定
DataSource設定: DataSourceConfiguration
前回と一緒ですので省略します。jdbcTemplateのbean定義は不要ですので削除しても構いませんし、そのまま残しておいても実害はありません。
Mirageの設定: MirageConfiguration
ちょっと凝った設定をいくつかしていますが、基本的にMirageの紹介でさっと説明したSpringの設定と同じです。
import jp.classmethod.example.berserker.DataAccessSample; import jp.sf.amateras.mirage.SqlManagerImpl; import jp.sf.amateras.mirage.bean.BeanDescFactory; import jp.sf.amateras.mirage.bean.FieldPropertyExtractor; import jp.sf.amateras.mirage.dialect.MySQLDialect; import jp.sf.amateras.mirage.integration.spring.SpringConnectionProvider; import jp.sf.amateras.mirage.naming.RailsLikeNameConverter; import jp.sf.amateras.mirage.provider.ConnectionProvider; import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mirage.repository.config.EnableMirageRepositories; import org.springframework.data.mirage.repository.support.MiragePersistenceExceptionTranslator; import org.springframework.jdbc.datasource.DataSourceTransactionManager; @Configuration @EnableMirageRepositories(basePackageClasses = DataAccessSample.class, sqlManagerRef = "sqlManager") public class MirageConfiguration { @Autowired DataSourceTransactionManager transactionManager; @Bean public SqlManagerImpl sqlManager() { // bridge java.util.logging used by mirage SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); SqlManagerImpl sqlManagerImpl = new SqlManagerImpl(); sqlManagerImpl.setConnectionProvider(connectionProvider()); sqlManagerImpl.setDialect(new MySQLDialect()); sqlManagerImpl.setBeanDescFactory(beanDescFactory()); sqlManagerImpl.setNameConverter(new RailsLikeNameConverter()); return sqlManagerImpl; } @Bean public ConnectionProvider connectionProvider() { SpringConnectionProvider springConnectionProvider = new SpringConnectionProvider(); springConnectionProvider.setTransactionManager(transactionManager); return springConnectionProvider; } @Bean public BeanDescFactory beanDescFactory() { BeanDescFactory beanDescFactory = new BeanDescFactory(); beanDescFactory.setPropertyExtractor(new FieldPropertyExtractor()); return beanDescFactory; } @Bean public MiragePersistenceExceptionTranslator persistenceExceptionTranslator() { return new MiragePersistenceExceptionTranslator(); } }
エンティティをSpring Data / Mirage用に修正
続いて、Mirageの紹介を参考に、前回作ったクラスにアノテーションを追加してください。また、主キーのフィールドには、mirageのアノテーションだけでなく、spring-dataのアノテーションIdを追加してください。
import jp.sf.amateras.mirage.annotation.Column; import jp.sf.amateras.mirage.annotation.PrimaryKey; import jp.sf.amateras.mirage.annotation.PrimaryKey.GenerationType; import jp.sf.amateras.mirage.annotation.Table; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.Id; @ToString @Table(name = "users") @AllArgsConstructor public class User { @Getter @Setter @Id @PrimaryKey(generationType = GenerationType.APPLICATION) @Column(name = "username") private String username; @Getter @Setter @Column(name = "password") private String password; }
ここでついでに、Pictureクラスも作ってしまいましょうか。
import java.io.Serializable; import jp.sf.amateras.mirage.annotation.Column; import jp.sf.amateras.mirage.annotation.PrimaryKey; import jp.sf.amateras.mirage.annotation.PrimaryKey.GenerationType; import jp.sf.amateras.mirage.annotation.Table; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.Id; @ToString @EqualsAndHashCode(of = "pictureId") @NoArgsConstructor(access = AccessLevel.PACKAGE) @Table(name = "pictures") @SuppressWarnings("serial") public class Picture implements Serializable { @Getter @Id @PrimaryKey(generationType = GenerationType.IDENTITY) @Column(name = "picture_id") private long pictureId; @Getter @Setter @NonNull @Column(name = "location") private String location; @Getter @Setter @Column(name = "event_id") private long eventId; public Picture(String location, long eventId) { this.location = location; this.eventId = eventId; } }
まず、エンティティクラスはDBに永続化される情報を保持するクラスとなりますので、Serializableを実装するのは妥当かと思います。(不要であれば敢えて実装する必要は無いことも多いですが。)
setterについては「必要のないsetterを定義しない(or 可視性を下げる)」ことにより安定します。例えばPicture#setPictureIdの定義は不要、もしくは最低でもpackage privateで用が足りるのではないでしょうか。ピクチャIDはDBの自動採番によりつけられるだけですので、クライアントサイドから明示的に設定する機会はほとんど無いはずです。こんな感じで、本当に必要になるまで定義しないようにしましょう。
また、equals及びhashCodeは、エンティティのIDのみに依存して同一性を判断する、という方針にします。toStringは適当に。
最後にコンストラクタについて。新規インスタンスの作成が楽になるようにコンストラクタを定義しましたが、Mirageはデフォルトコンストラクタを要求 *6します。Pictureのインスタンスは、クライアントが生成するだけではなく、MirageがDBから引き当てた情報をマッピングするために生成することもあります。そのため、デフォルトコンストラクタを(publicでなくても構わないので)必ず定義しておくと良いでしょう。
リポジトリインターフェイスの定義
ひとまずこれだけです。これ以上ないくらいに簡単ですね。
import org.springframework.data.mirage.repository.MirageRepository; public interface UserRepository extends MirageRepository<User, String> { }
import org.springframework.data.mirage.repository.MirageRepository; public interface UserRepository extends MirageRepository<Picture, Long> { }
実行してみる
ではメインのJavaコードです。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component @ComponentScan public class DataAccessSample { private static Logger logger = LoggerFactory.getLogger(DataAccessSample.class); public static void main(String[] args) { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(DataAccessSample.class)) { DataAccessSample das = context.getBean(DataAccessSample.class); das.execute(); } } @Autowired UserRepository userRepos; @Transactional public void execute() { boolean flag = userRepos.exists("yokota"); // create logger.info("Create user"); if (flag) { userRepos.save(new User("watanabe", "$2a$10$MHPqWJ61alnBlUbvjEGK/uWRvwtYzolWCuFXW8YMJkT54HUB0H9iq")); } else { userRepos.save(new User("yokota", "$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm")); } // read logger.info("List user"); Iterable<User> all = userRepos.findAll(); for (User user : all) { logger.info(" {}", user); } logger.info("Get user"); User miyamoto = userRepos.findOne("miyamoto"); logger.info(" {}", miyamoto); // update logger.info("Update user"); miyamoto.setPassword("$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC"); userRepos.save(miyamoto); // delete logger.info("Delete user"); if (flag) { userRepos.delete("yokota"); } else { userRepos.delete("watanabe"); } } }
UserRepositoryをDIして、各種メソッドでCRUD(作成、全件・一件検索、更新、削除)を実行してみました。簡単ですね。注目すべきポイントは「ここまでSQL無し」ということです。
サンプルプロジェクト berserker v5.0
2015-07-13追記:このコードを、GitHubに上げておきました。ご興味のある方は、下記のように実行してみてください。
$ git clone https://github.com/classmethod-sandbox/berserker.git $ cd berserker $ git checkout 5.0 $ ./gradlew execute :compileJava UP-TO-DATE :processResources UP-TO-DATE :classes UP-TO-DATE :execute 2016/04/09 10:55:55.483 [main] INFO o.s.c.a.AnnotationConfigApplicationContext:578 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@71be98f5: startup date [Sat Apr 09 10:55:55 JST 2016]; root of context hierarchy 2016/04/09 10:55:56.540 [main] INFO o.s.j.d.DriverManagerDataSource:133 - Loaded JDBC driver: com.mysql.jdbc.Driver 2016/04/09 10:55:57.422 [main] INFO j.c.e.berserker.DataAccessSample:54 - Create user 2016/04/09 10:55:57.438 [main] INFO j.c.e.berserker.DataAccessSample:62 - List user 2016/04/09 10:55:57.443 [main] INFO j.c.e.berserker.DataAccessSample:65 - User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC) 2016/04/09 10:55:57.446 [main] INFO j.c.e.berserker.DataAccessSample:65 - User(username=watanabe, password=$2a$10$MHPqWJ61alnBlUbvjEGK/uWRvwtYzolWCuFXW8YMJkT54HUB0H9iq) 2016/04/09 10:55:57.446 [main] INFO j.c.e.berserker.DataAccessSample:65 - User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm) 2016/04/09 10:55:57.447 [main] INFO j.c.e.berserker.DataAccessSample:68 - Get user 2016/04/09 10:55:57.451 [main] INFO j.c.e.berserker.DataAccessSample:70 - User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC) 2016/04/09 10:55:57.451 [main] INFO j.c.e.berserker.DataAccessSample:73 - Update user 2016/04/09 10:55:57.456 [main] INFO j.c.e.berserker.DataAccessSample:78 - Delete user 2016/04/09 10:55:57.471 [main] INFO o.s.c.a.AnnotationConfigApplicationContext:960 - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@71be98f5: startup date [Sat Apr 09 10:55:55 JST 2016]; root of context hierarchy BUILD SUCCESSFUL Total time: 5.06 secs
特殊な検索メソッドを実装する
まぁ、SQL無しとは言え、ここまでの操作は主キーによる操作ばかりでした。実際のアプリケーションでは、もっと特殊な検索条件が必要になるでしょう。ということで、まずは腕ならしということで、(あまり意味のない検索条件ですが)「ユーザ名がX文字以下のユーザ」を探すメソッドを作ってみましょう。ついでに、ユーザ数が多くなった時のことを想定して、ページングも実装してみましょう。ということでUserRepositoryに以下のようにfindByUsernameMaxLengthメソッドを追加します。
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mirage.repository.MirageRepository; import org.springframework.data.repository.query.Param; public interface UserRepository extends MirageRepository<User, String> { Page<User> findByUsernameMaxLength(@Param("username_length") int len, Pageable pageable); }
以上の通り、@Param("username_length")というアノテーションを付けた第1引数が、SQL内でのusername_lengthパラメータにマッピングされています。複数の@Paramアノテーションを使えば、複数の引数をパラメータにマッピングできますし、下記のようにListをSQLのIN句にマッピングすることも可能です。
@Param("usernames") List<String> usernames
... username IN /*usernames*/('foo', 'bar') ...
また、引数にPageableを含めた場合、戻り値としてPage型が使えます。Pageableは、大量にある要素のうち、一部を切り出して来る「ページング」を指示するオブジェクトです。Pageableについては、後でもう少し詳しく説明します。
一方、戻り値のPageは、全体の中の一部を表すオブジェクトで、getContent()メソッドにより、部分のリストを取得できます。
メソッドに対応するSQLを記述する
続いてSQLの定義です。リポジトリの場所と同じパッケージ内に、リポジトリインターフェイス名 + "_" + メソッド名 + ".sql"というファイル名でSQLファイルを作ります。
src/main/resources/jp/classmethod/example/example/model/UserRepository_findByUsernameMaxLength.sql
SELECT * FROM users WHERE CHAR_LENGTH(username) <= /*username_length*/10 /*IF orders != null*/ ORDER BY /*$orders*/username -- ELSE ORDER BY username /*END*/ /*BEGIN*/ LIMIT /*IF offset != null*/ /*offset*/0, /*END*/ /*IF size != null*/ /*size*/10 /*END*/ /*END*/
Pageableの基本実装はPageRequestクラスで、以下のように使います。第1引数はページ番号(0-based)、第2引数は1ページ当たりの要素数、第3引数(省略可)はソート条件を与えます。
Pageable pageable = new PageRequest(0, 100, new Sort( new Order(Direction.DESC, "CHAR_LENGTH(username)"), new Order(Direction.ASC, "username")));
このように指定することにより、Mirageのパラメータとしてorders = "CHAR_LENGTH(username) DESC, username ASC"、offset = 0、size = 100が設定され、ORDER BY句とLIMIT句が有効になります。上記SQLのORDER BY以降の部分については、spring-data-mirageにおけるイディオムのようなものなので、何度もコピーして使う機会があるとおもいます。
ちなみに、/*$orders*/の部分について、先頭に$マークがあるのに気付いた方がいるかもしれません。Mirageの紹介記事でも少し触れた通り、Mirageは、パラメータの当て込みにPreparedStatementの仕組みを使います。しかし、ORDER BY句はORDER BY ?と記述しても、ソート条件をパラメータとして当て込むことができません。従って、PreparedStatementの手前で文字列置換としてパラメータを当て込みます。外部入力値等をこの仕組みで当て込む場合、適切なエスケープ処理を行わなければSQLインジェクション脆弱性が発生するポイントとなってしまいますので、注意してください。つまり、今回の例でもOrderの第2引数に、外部入力値を当て込むのは危険です。本稿の例は、値をハードコーディングしているので脆弱性はありませんが、誤った使い方は脆弱性に繋がりますので、セキュリティ上気をつけなければならないポイントとして意識しておいてください。
findByUsernameMaxLengthを使ってみる
あとはクライアントから普通に呼び出すだけです。
Page<User> p = userRepos.findByUsernameMaxLength(6, new PageRequest(0, 2)); List<User> users = p.getContent(); assert users.size() <= 2;
1ページあたり2件のページングを指定したため、要素数は2以下になるはずですね。
サンプルプロジェクト berserker v5.1
2015-07-13追記:このコードを、GitHubに上げておきました。ご興味のある方は、下記のように実行してみてください。
$ git clone https://github.com/classmethod-sandbox/berserker.git $ cd berserker $ git checkout 5.1 $ ./gradlew execute :compileJava :processResources :classes :execute 2016/04/09 10:57:22.798 [main] INFO o.s.c.a.AnnotationConfigApplicationContext:578 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@96532d6: startup date [Sat Apr 09 10:57:22 JST 2016]; root of context hierarchy 2016/04/09 10:57:23.779 [main] INFO o.s.j.d.DriverManagerDataSource:133 - Loaded JDBC driver: com.mysql.jdbc.Driver 2016/04/09 10:57:25.034 [main] INFO j.c.e.berserker.DataAccessSample:58 - Create user 2016/04/09 10:57:25.048 [main] INFO j.c.e.berserker.DataAccessSample:66 - List user 2016/04/09 10:57:25.051 [main] INFO j.c.e.berserker.DataAccessSample:69 - User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC) 2016/04/09 10:57:25.053 [main] INFO j.c.e.berserker.DataAccessSample:69 - User(username=watanabe, password=$2a$10$MHPqWJ61alnBlUbvjEGK/uWRvwtYzolWCuFXW8YMJkT54HUB0H9iq) 2016/04/09 10:57:25.053 [main] INFO j.c.e.berserker.DataAccessSample:69 - User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm) 2016/04/09 10:57:25.053 [main] INFO j.c.e.berserker.DataAccessSample:72 - List user filtered by length 2016/04/09 10:57:25.066 [main] INFO j.c.e.berserker.DataAccessSample:77 - User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm) 2016/04/09 10:57:25.067 [main] INFO j.c.e.berserker.DataAccessSample:80 - Get user 2016/04/09 10:57:25.071 [main] INFO j.c.e.berserker.DataAccessSample:82 - User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC) 2016/04/09 10:57:25.072 [main] INFO j.c.e.berserker.DataAccessSample:85 - Update user 2016/04/09 10:57:25.080 [main] INFO j.c.e.berserker.DataAccessSample:90 - Delete user 2016/04/09 10:57:25.093 [main] INFO o.s.c.a.AnnotationConfigApplicationContext:960 - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@96532d6: startup date [Sat Apr 09 10:57:22 JST 2016]; root of context hierarchy BUILD SUCCESSFUL Total time: 6.828 secs
まとめ
さて、いかがでしたでしょうか。素の状態のMirage SQLは、「SQLファイルをどのように整理すべきか」や「ページングをどのように行うか」等がユーザ側に委ねられていました。これに対してspring-data-mirageは、各エンティティに対するリポジトリを定義し、ある程度型にはまった操作(findOne等)をデフォルトで提供します。また、特殊な検索については、メソッド毎にSQLファイルを定義して、複雑なクエリを実行できるようになっています。
今後、テーブル(エンティティ)を増やす場合においては、まずエンティティクラスを定義し、それに対するリポジトリを宣言する、ということの繰り返しになると思います。
で、以上のコードについて、ここで冷静に考えてみてください。SpringDataMirageSampleにDIされたuserReposは、どのクラスのインスタンスなのでしょうか。UserRepositoryは自作のインターフェイスであり、今回、その実装クラスは書いていません! それなのに、なぜ上記コードがきちんと動くのでしょうか。
spring-data-mirageでは、インターフェイスを定義するだけで、実装はどこからとも無く勝手にやってきます。仮にfindByUsernameLengthメソッドの中身をデバッガで追ったら何が起こるのか、多くの方には想像がつかないと思います。…これが黒魔術です。
参考までに、何らかのトラブルでfindByUsernameLengthメソッドの中身を追う必要が出て来てしまったらorg.springframework.data.mirage.repository.query.MirageQuery#executeメソッドの入り口にブレイクポイントを張るなりしてみると良いと思います。
さて。今回は@Transactionalのアノテーションについて、さらっと流してしまいましたが。次回はSpringによるトランザクション制御について解説したいと思います。このトランザクション制御の仕組みも少々黒魔術的です。なので、トランザクションの解説と共に、黒魔術のざっくりとした種明かし(どのようなテクニックを使って、不思議な状況を実現しているのか)も披露できればと思っています。
お楽しみに!
脚注
- 裏側でどんな処理を行っているのか、という想像のしやすさ、コードの追いやすさ。 ↩
- この発言は個人の見解であり、所属する組織の公式見解ではありません。 ↩
- あくまで業務としてではなく、個人的に作ってたものですが。 ↩
- Spring DataのCommunity Projectsに載せてもらうのってどうやるんだろ。まずMirage SQLがMaven centralに載ってないと難しいかも? 可能であるならSpring配下でメンテナンスしたいとは思っているのですが。 ↩
- mirageはともかく、spring-data-mirageは、正規の本家プロジェクトではないのにorg.springframeworkというgroupIdやパッケージを使ってしまっているので、色々手続き踏まないと難しそうです。 ↩
- 正確には、全ての引数にnullや0等を渡すコンストラクタ呼び出しによるインスタンス化も試みますが、今回の例だとlocationがnullですので、コンストラクタの呼び出しに失敗してしまいます。 ↩